
<template>
  <!-- 词云图 -->
  <div id="wordCloud" class="chart" width="500" height="700"></div>
</template>
<script>
import G6 from "@antv/g6";
const lightColors = [
  "#8FE9FF",
  "#81EAEF",
  "#FFC9E3",
  "#A7C2FF",
  "#FFA1E3",
  "#FFE269",
  "#BFCFEE",
  "#FFA0C5",
  "#D5FF86",
];
const darkColors = [
  "#7DA8FF",
  "#46E6C1",
  "#FF68A7",
  "#7F86FF",
  "#AE6CFF",
  "#FF5A34",
  "#5D7092",
  "#FF6565",
  "#6BFFDE",
];
const uLightColors = [
  "#CFF6FF",
  "#BCFCFF",
  "#FFECF5",
  "#ECFBFF",
  "#EAD9FF",
  "#FFF8DA",
  "#DCE2EE",
  "#FFE7F0",
  "#EEFFCE",
];
const uDarkColors = [
  "#CADBFF",
  "#A9FFEB",
  "#FFC4DD",
  "#CACDFF",
  "#FFD4F2",
  "#FFD3C9",
  "#EBF2FF",
  "#FFCBCB",
  "#CAFFF3",
];

export default {
  props: {
    chartId: {
      type: String,
    },
    chartData: {
      type: Array,
    },
  },
  data() {
    return {
      showNodes: [],
      showEdges: [],
      curShowNodes: [],
      curShowEdges: [],
      nodes: [],
      edges: [],
      nodeMap: new Map(),
      edgesMap: new Map(),
      curShowNodesMap: new Map(),
      highlighting: false,
      currentFocus: null,
      LIMIT_OVERFLOW_WIDTH: null,
      LIMIT_OVERFLOW_HEIGHT: null,
      width: 0,
      height: 0,
      graph: null,
      gColors: [],
      mchartData: {
        nodes: [],
      },
    };
  },
  watch: {
    chartData(val) {
      this.mchartData = val;
      this.getData();
    },
  },
  mounted() {
    this.renderChart();
    this.width = document.getElementById("wordCloud").scrollWidth;
    this.height = document.getElementById("wordCloud").scrollHeight || 500;
    const unlightColorMap = new Map();
    lightColors.forEach((lcolor, i) => {
      this.gColors.push("l(0) 0:" + lcolor + " 1:" + darkColors[i]);
      unlightColorMap.set(
        this.gColors[i],
        "l(0) 0:" + uLightColors[i] + " 1:" + uDarkColors[i]
      );
    });
  },
  methods: {
    getData() {
      this.daobj = {
        nodes: [],
      };
      this.chartData &&
        this.chartData.map((item) => {
          this.daobj.nodes.push({
            id: item.key,
            name: item.key,
            level: 0,
            childrenNum: item.doc_count,
          });
        });

      if (this.daobj.nodes && this.daobj.nodes.length > 0) {
        this.loadData(this.daobj);
      }
    },
    renderChart() {
      const layoutCfg = {
        type: "force",
        nodeSize: (d) => {
          return d.size / 2 + 5;
        },
        nodeStrength: 2500,
        collideStrength: 0.8,
        alphaDecay: 0.01,
        preventOverlap: true,
        onTick: () => {
          const nodeItems = this.graph.getNodes();
          const height = this.graph.get("height");
          const width = this.graph.get("width");
          const padding = 10;
          nodeItems.forEach((item) => {
            const model = item.getModel();
            if (model.x > width - padding) model.x = width - padding;
            else if (model.x < padding) model.x = padding;

            if (model.y > height - padding) model.y = height - padding;
            else if (model.y < padding) model.y = padding;
          });
        },
      };
      G6.registerBehavior("double-finger-drag-canvas", {
        getEvents: function getEvents() {
          return {
            wheel: "onWheel",
          };
        },
        onWheel: (ev) => {
          if (ev.ctrlKey) {
            const canvas = this.graph.get("canvas");
            const point = canvas.getPointByClient(ev.clientX, ev.clientY);
            let ratio = this.graph.getZoom();
            if (ev.wheelDelta > 0) {
              ratio = ratio + ratio * 0.05;
            } else {
              ratio = ratio - ratio * 0.05;
            }
            this.graph.zoomTo(ratio, {
              x: point.x,
              y: point.y,
            });
          } else {
            const x = ev.deltaX || ev.movementX;
            const y = ev.deltaY || ev.movementY || (-ev.wheelDelta * 125) / 3;
            this.translate(x, y);
          }
          ev.preventDefault();
        },
      });
      G6.registerNode(
        "bubble",
        {
          drawShape(cfg, group) {
            const self = this;
            const r = cfg.size / 2;
            // a circle by path
            const path = [
              ["M", -r, 0],
              ["C", -r, r / 2, -r / 2, r, 0, r],
              ["C", r / 2, r, r, r / 2, r, 0],
              ["C", r, -r / 2, r / 2, -r, 0, -r],
              ["C", -r / 2, -r, -r, -r / 2, -r, 0],
              ["Z"],
            ];
            const keyShape = group.addShape("path", {
              attrs: {
                x: 0,
                y: 0,
                path,
                fill: cfg.color || "steelblue",
              },
              // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
              name: "path-shape",
            });

            const mask = group.addShape("path", {
              attrs: {
                x: 0,
                y: 0,
                path,
                opacity: 0.25,
                fill: cfg.color || "steelblue",
                shadowColor: cfg.color.split(" ")[2].substr(2),
                shadowBlur: 40,
                shadowOffsetX: 0,
                shadowOffsetY: 30,
              },
              // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
              name: "mask-shape",
            });

            const spNum = 10; // split points number
            const directions = [],
              rs = [];
            self.changeDirections(spNum, directions);
            for (let i = 0; i < spNum; i++) {
              const rr = r + directions[i] * ((Math.random() * r) / 1000); // +-r/6, the sign according to the directions
              if (rs[i] < 0.97 * r) rs[i] = 0.97 * r;
              else if (rs[i] > 1.03 * r) rs[i] = 1.03 * r;
              rs.push(rr);
            }
            keyShape.animate(
              () => {
                const path = self.getBubblePath(r, spNum, directions, rs);
                return { path };
              },
              {
                repeat: true,
                duration: 10000,
              }
            );

            const directions2 = [],
              rs2 = [];
            self.changeDirections(spNum, directions2);
            for (let i = 0; i < spNum; i++) {
              const rr = r + directions2[i] * ((Math.random() * r) / 1000); // +-r/6, the sign according to the directions
              if (rs2[i] < 0.97 * r) rs2[i] = 0.97 * r;
              else if (rs2[i] > 1.03 * r) rs2[i] = 1.03 * r;
              rs2.push(rr);
            }
            mask.animate(
              () => {
                const path = self.getBubblePath(r, spNum, directions2, rs2);
                return { path };
              },
              {
                repeat: true,
                duration: 10000,
              }
            );
            return keyShape;
          },
          changeDirections(num, directions) {
            for (let i = 0; i < num; i++) {
              if (!directions[i]) {
                const rand = Math.random();
                const dire = rand > 0.5 ? 1 : -1;
                directions.push(dire);
              } else {
                directions[i] = -1 * directions[i];
              }
            }
            return directions;
          },
          getBubblePath(r, spNum, directions, rs) {
            const path = [];
            const cpNum = spNum * 2; // control points number
            const unitAngle = (Math.PI * 2) / spNum; // base angle for split points
            let angleSum = 0;
            const sps = [];
            const cps = [];
            for (let i = 0; i < spNum; i++) {
              const speed = 0.001 * Math.random();
              rs[i] = rs[i] + directions[i] * speed * r; // +-r/6, the sign according to the directions
              if (rs[i] < 0.97 * r) {
                rs[i] = 0.97 * r;
                directions[i] = -1 * directions[i];
              } else if (rs[i] > 1.03 * r) {
                rs[i] = 1.03 * r;
                directions[i] = -1 * directions[i];
              }
              const spX = rs[i] * Math.cos(angleSum);
              const spY = rs[i] * Math.sin(angleSum);
              sps.push({ x: spX, y: spY });
              for (let j = 0; j < 2; j++) {
                const cpAngleRand = unitAngle / 3;
                const cpR = rs[i] / Math.cos(cpAngleRand);
                const sign = j === 0 ? -1 : 1;
                const x = cpR * Math.cos(angleSum + sign * cpAngleRand);
                const y = cpR * Math.sin(angleSum + sign * cpAngleRand);
                cps.push({ x, y });
              }
              angleSum += unitAngle;
            }
            path.push(["M", sps[0].x, sps[0].y]);
            for (let i = 1; i < spNum; i++) {
              path.push([
                "C",
                cps[2 * i - 1].x,
                cps[2 * i - 1].y,
                cps[2 * i].x,
                cps[2 * i].y,
                sps[i].x,
                sps[i].y,
              ]);
            }
            path.push([
              "C",
              cps[cpNum - 1].x,
              cps[cpNum - 1].y,
              cps[0].x,
              cps[0].y,
              sps[0].x,
              sps[0].y,
            ]);
            path.push(["Z"]);
            return path;
          },
          setState(name, value, item) {
            const shape = item.get("keyShape");
            if (name === "dark") {
              if (value) {
                if (shape.attr("fill") !== "#fff") {
                  shape.oriFill = shape.attr("fill");
                  const uColor = this.unlightColorMap.get(shape.attr("fill"));
                  shape.attr("fill", uColor);
                } else {
                  shape.attr("opacity", 0.2);
                }
              } else {
                if (shape.attr("fill") !== "#fff") {
                  shape.attr("fill", shape.oriFill || shape.attr("fill"));
                } else {
                  shape.attr("opacity", 1);
                }
              }
            }
          },
        },
        "single-node"
      );
      G6.registerNode(
        "animate-circle",
        {
          setState(name, value, item) {
            const shape = item.get("keyShape");
            const label = shape.get("parent").get("children")[1];
            if (name === "disappearing" && value) {
              shape.animate(
                (ratio) => {
                  return {
                    opacity: 1 - ratio,
                    r: shape.attr("r") * (1 - ratio),
                  };
                },
                {
                  duration: 200,
                }
              );
              label.animate(
                (ratio) => {
                  return {
                    opacity: 1 - ratio,
                  };
                },
                {
                  duration: 500,
                }
              );
            } else if (name === "appearing" && value) {
              const r = item.getModel().size / 2;
              shape.animate(
                (ratio) => {
                  return {
                    opacity: ratio,
                    r: r * ratio,
                    fill: shape.attr("fill"),
                  };
                },
                {
                  duration: 300,
                }
              );
              label.animate(
                {
                  onFrame(ratio) {
                    return {
                      opacity: ratio,
                    };
                  },
                },
                {
                  duration: 300,
                }
              );
            } else if (name === "dark") {
              if (value) {
                if (shape.attr("fill") !== "#fff") {
                  shape.oriFill = shape.attr("fill");
                  const uColor = this.unlightColorMap.get(shape.attr("fill"));
                  shape.attr("fill", uColor);
                } else {
                  shape.attr("opacity", 0.2);
                  label.attr("fill", "#A3B1BF");
                }
              } else {
                if (shape.attr("fill") !== "#fff") {
                  shape.attr("fill", shape.oriFill || shape.attr("fill"));
                } else {
                  shape.attr("opacity", 1);
                  label.attr("fill", "#697B8C");
                }
              }
            }
          },
        },
        "circle"
      );
      G6.registerEdge(
        "animate-line",
        {
          drawShape(cfg, group) {
            const self = this;
            let shapeStyle = self.getShapeStyle(cfg);
            shapeStyle = Object.assign(shapeStyle, {
              opacity: 0,
              strokeOpacity: 0,
            });
            const keyShape = group.addShape("path", {
              attrs: shapeStyle,
              // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
              name: "path-shape",
            });
            return keyShape;
          },
          afterDraw(cfg, group) {
            const shape = group.get("children")[0];
            shape.animate(
              (ratio) => {
                const opacity = ratio * cfg.style.opacity;
                const strokeOpacity = ratio * cfg.style.strokeOpacity;
                return {
                  opacity: ratio || opacity,
                  strokeOpacity: ratio || strokeOpacity,
                };
              },
              {
                duration: 300,
              }
            );
          },
          setState(name, value, item) {
            const shape = item.get("keyShape");
            if (name === "disappearing" && value) {
              shape.animate(
                (ratio) => {
                  return {
                    opacity: 1 - ratio,
                    strokeOpacity: 1 - ratio,
                  };
                },
                {
                  duration: 200,
                }
              );
            } else if (name === "dark") {
              if (value) shape.attr("opacity", 0.2);
              else shape.attr("opacity", 1);
            }
          },
        },
        "line"
      );
      this.graph = new G6.Graph({
        container: "wordCloud",
        width: this.width,
        height: this.height,
        linkCenter: true,
        layout: layoutCfg,
        modes: {
          default: ["drag-canvas"],
        },
        defaultNode: {
          type: "bubble",
          size: 30,
        },
        defaultEdge: {
          color: "#888",
          type: "animate-line", //'animate-line'
        },
      });
      this.graph.get("canvas").set("localRefresh", false);
      this.graph.on("node:mouseenter", (e) => {
        const item = e.item;
        const model = item.getModel();
        if (model.level === 0) {
          return;
        }
        this.highlighting = true;
        this.graph.setAutoPaint(false);
        const nodeItems = this.graph.getNodes();
        const edgeItems = this.graph.getEdges();
        nodeItems.forEach((node) => {
          this.graph.setItemState(node, "dark", true);
          node.getModel().light = false;
        });
        this.graph.setItemState(item, "dark", false);
        model.light = true;
        const tags = model.tags;
        const findTagsMap = new Map();
        let mid = 0;

        let fTag = "";
        // if the model is F node, find the leaves of it
        if (!model.isLeaf && model.level !== 0) {
          fTag = model.tag;
          nodeItems.forEach((item) => {
            const itemModel = item.getModel();
            if (!itemModel.isLeaf) return;
            const modelTags = itemModel.tags;
            modelTags.forEach((mt) => {
              const mts = mt.split("-");
              if (mts[1] === fTag) {
                this.graph.setItemState(item, "dark", false);
                itemModel.light = true;
              }
            });
          });
        }

        // find the tags
        tags.forEach((t) => {
          const ts = t.split("-");
          findTagsMap.set(ts[0], mid);
          mid++;
          if (ts[1]) {
            findTagsMap.set(ts[1], mid);
            mid++;
          }
        });
        // find the nodes with tag === tags[?]
        nodeItems.forEach((item) => {
          const node = item.getModel();
          if (findTagsMap.get(node.tag) !== undefined) {
            this.graph.setItemState(item, "dark", false);
            node.light = true;
          }
        });
        edgeItems.forEach((item) => {
          const source = item.getSource().getModel();
          const target = item.getTarget().getModel();
          if (source.light && target.light) {
            this.graph.setItemState(item, "dark", false);
          } else {
            this.graph.setItemState(item, "dark", true);
          }
        });
        this.graph.paint();
        this.graph.setAutoPaint(true);
      });

      this.graph.on("node:mouseleave", () => {
        if (this.highlighting) {
          const nodeItems = this.graph.getNodes();
          const edgeItems = this.graph.getEdges();
          this.highlighting = false;
          nodeItems.forEach((item) => {
            this.graph.setItemState(item, "dark", false);
          });
          edgeItems.forEach((item) => {
            this.graph.setItemState(item, "dark", false);
          });
        }
      });

      this.graph.on("node:dragstart", (e) => {
        this.graph.layout();
        this.refreshDragedNodePosition(e);
      });
      this.graph.on("node:drag", (e) => {
        this.refreshDragedNodePosition(e);
      });
      this.graph.on("node:dragend", (e) => {
        e.item.get("model").fx = null;
        e.item.get("model").fy = null;
      });
    },
    getEchartImg(base) {
      window.echartImg = base;
    },
    mapNodeSize(nodes, propertyName, visualRange) {
      let minp = 9999999999;
      let maxp = -9999999999;
      nodes.forEach((node) => {
        minp = node[propertyName] < minp ? node[propertyName] : minp;
        maxp = node[propertyName] > maxp ? node[propertyName] : maxp;
      });
      const rangepLength = maxp - minp;
      const rangevLength = visualRange[1] - visualRange[0];
      nodes.forEach((node) => {
        node.size =
          ((node[propertyName] - minp) / rangepLength) * rangevLength +
          visualRange[0];
      });
    },
    translate(x, y) {
      let moveX = x;
      let moveY = y;

      /* 获得当前偏移量*/
      const group = this.graph.get("group");
      const bbox = group.getBBox();
      const leftTopPoint = this.graph.getCanvasByPoint(bbox.minX, bbox.minY);
      const rightBottomPoint = this.graph.getCanvasByPoint(
        bbox.maxX,
        bbox.maxY
      );
      /* 如果 x 轴在区域内，不允许左右超过100 */
      if (x < 0 && leftTopPoint.x - x > this.LIMIT_OVERFLOW_WIDTH) {
        moveX = 0;
      }
      if (
        x > 0 &&
        rightBottomPoint.x - x < this.width - this.LIMIT_OVERFLOW_WIDTH
      ) {
        moveX = 0;
      }

      if (y < 0 && leftTopPoint.y - y > this.LIMIT_OVERFLOW_HEIGHT) {
        moveY = 0;
      }
      if (
        y > 0 &&
        rightBottomPoint.y - y < this.height - this.LIMIT_OVERFLOW_HEIGHT
      ) {
        moveY = 0;
      }
      this.graph.translate(-moveX, -moveY);
    },
    refreshDragedNodePosition(e) {
      const model = e.item.get("model");
      model.fx = e.x;
      model.fy = e.y;
    },
    loadData(data) {
      const layoutController = this.graph.get("layoutController");
      layoutController.layoutCfg.nodeStrength = 2500;
      layoutController.layoutCfg.collideStrength = 0.8;
      layoutController.layoutCfg.alphaDecay = 0.01;
      this.nodes = data.nodes;
      // this.edges = data.edges;

      this.showNodes = [];
      this.showEdges = [];
      this.nodeMap = new Map();
      this.edgesMap = new Map();
      // find the roots
      this.nodes.forEach((node) => {
        if (node.level === 0) {
          node.color =
            this.gColors[this.showNodes.length % this.gColors.length];
          node.style = {
            fill: this.gColors[this.showNodes.length % this.gColors.length],
            lineWidth: 0,
          };
          node.labelCfg = {
            style: {
              fontSize: 14,
              fill: "#fff",
              fontWeight: 700,
            },
          };
          node.x = Math.random() * 800;
          node.y = Math.random() * 800;
          this.showNodes.push(node);
        }
        if (!node.isLeaf) {
          const num = node.childrenNum ? `\n(${node.childrenNum})` : "";
          node.label = `${node.name}${num}`;
        } else {
          node.label = node.name;
        }
        this.nodeMap.set(node.id, node);
      });

      this.mapNodeSize(this.showNodes, "childrenNum", [70, 120]);

      // map the color to F nodes, same to its parent
      this.nodes.forEach((node) => {
        if (node.level !== 0 && !node.isLeaf) {
          const parent = this.nodeMap.get(node.tags[0]);
          node.color = parent.color;
          node.style = {
            fill: "#fff",
            fontWeight: 300,
          };
        }
      });
      // this.edges.forEach((edge) => {
      //   // map the id
      //   edge.id = `${edge.source}-${edge.target}`;
      //   edge.style = {
      //     lineWidth: 0.5,
      //     opacity: 1,
      //     strokeOpacity: 1,
      //   };
      //   this.edgesMap.set(edge.id, edge);
      // });
      this.graph.clear(); // 清空画布
      this.graph.changeData({
        nodes: this.showNodes,
        edges: [],
      });
      this.graph.render();
    },
  },

  destroyed() {},
};
</script>

<style lang="scss" scoped>
#wordCloud {
  width: 100%;
  height: 800px;
}
</style>



